define([
        "jquery",
        "underscore",
        "storage",
        "crel",
        "xregexp",
        "stacktrace",
        "FileSaver"
        ], function($, _, storage, crel, XRegExp, printStackTrace, saveAs) {

            var utils = {};

            utils.msie = (function() {
                /**
                 * IE 11 changed the format of the UserAgent string.
                 * See http://msdn.microsoft.com/en-us/library/ms537503.aspx
                 */
                var msie = parseInt((/msie (\d+)/.exec(navigator.userAgent.toLowerCase()) || [])[1], 10);
                if (isNaN(msie)) {
                    msie = parseInt((/trident\/.*; rv:(\d+)/.exec(navigator.userAgent.toLowerCase()) || [])[1], 10);
                }
                return msie;
            })();

            utils.urlResolve = (function() {
                var urlParsingNode = document.createElement("a");
                return function urlResolve(url) {
                    var href = url;

                    if (utils.msie) {
                        // Normalize before parse.  Refer Implementation Notes on why this is
                        // done in two steps on IE.
                        urlParsingNode.setAttribute("href", href);
                        href = urlParsingNode.href;
                    }

                    urlParsingNode.setAttribute('href', href);

                    // urlParsingNode provides the UrlUtils interface - http://url.spec.whatwg.org/#urlutils
                    return {
                        href: urlParsingNode.href,
                protocol: urlParsingNode.protocol ? urlParsingNode.protocol.replace(/:$/, '') : '',
                host: urlParsingNode.host,
                search: urlParsingNode.search ? urlParsingNode.search.replace(/^\?/, '') : '',
                hash: urlParsingNode.hash ? urlParsingNode.hash.replace(/^#/, '') : '',
                hostname: urlParsingNode.hostname,
                port: urlParsingNode.port,
                pathname: (urlParsingNode.pathname.charAt(0) === '/') ?
                    urlParsingNode.pathname : '/' + urlParsingNode.pathname
                    };
                };
            })();

            // Faster than setTimeout (see http://dbaron.org/log/20100309-faster-timeouts)
            utils.defer = (function() {
                var timeouts = [];
                var messageName = "deferMsg";
                window.addEventListener("message", function(evt) {
                    if(evt.source == window && evt.data == messageName) {
                        evt.stopPropagation();
                        if(timeouts.length > 0) {
                            timeouts.shift()();
                        }
                    }
                }, true);
                return function(fn) {
                    timeouts.push(fn);
                    window.postMessage(messageName, "*");
                };
            })();

            // Implements underscore debounce using our defer function
            utils.debounce = function(func, context) {
                var isExpected = false;

                function later() {
                    isExpected = false;
                    func.call(context);
                }

                return function() {
                    if(isExpected === true) {
                        return;
                    }
                    isExpected = true;
                    utils.defer(later);
                };
            };

            // Generates a 24 chars length random string (should be enough to prevent collisions)
            utils.randomString = (function() {
                var max = Math.pow(36, 6);

                function s6() {
                    // Linear [0-9a-z]{6} random string
                    return ('000000' + (Math.random() * max | 0).toString(36)).slice(-6);
                }

                return function() {
                    return [
                s6(),
                s6(),
                s6(),
                s6()
                ].join('');
                };
            })();

            // Return a parameter from the URL
            utils.getURLParameter = function(name) {
                // Parameter can be either a search parameter (&name=...) or a hash fragment parameter (#!name=...)
                var regex = new RegExp("(?:\\?|\\#\\!|&)" + name + "=(.+?)(?:&|\\#|$)");
                try {
                    return decodeURIComponent(regex.exec(location.search + location.hash)[1]);
                }
                catch(e) {
                    return undefined;
                }
            };

            // Transform a selector into a jQuery object
            function jqElt(element) {
                if(_.isString(element) || !element.val) {
                    return $(element);
                }
                return element;
            }

            // For input control
            function inputError(element, event) {
                if(event !== undefined) {
                    element.stop(true, true).addClass("error").delay(3000).queue(function() {
                        $(this).removeClass("error");
                        $(this).dequeue();
                    });
                    event.stopPropagation();
                }
            }

            // Return input value
            utils.getInputValue = function(element) {
                element = jqElt(element);
                return element.val();
            };

            // Set input value
            utils.setInputValue = function(element, value) {
                element = jqElt(element);
                element.val(value);
            };

            // Return input text value
            utils.getInputTextValue = function(element, event, validationRegex) {
                element = jqElt(element);
                var value = element.val();
                if(value === undefined) {
                    inputError(element, event);
                    return undefined;
                }
                // trim
                value = utils.trim(value);
                if((value.length === 0) || (validationRegex !== undefined && !value.match(validationRegex))) {
                    inputError(element, event);
                    return undefined;
                }
                return value;
            };

            // Return input number value
            function getInputNumValue(isFloat, element, event, min, max) {
                element = jqElt(element);
                var value = utils.getInputTextValue(element, event);
                if(value === undefined) {
                    return undefined;
                }
                value = isFloat ? parseFloat(value) : parseInt(value, 10);
                if(isNaN(value) || (min !== undefined && value < min) || (max !== undefined && value > max)) {
                    inputError(element, event);
                    return undefined;
                }
                return value;
            }

            // Return input integer value
            utils.getInputIntValue = _.partial(getInputNumValue, false);

            // Return input float value
            utils.getInputFloatValue = _.partial(getInputNumValue, true);

            // Return input value and check that it's a valid RegExp
            utils.getInputRegExpValue = function(element, event) {
                element = jqElt(element);
                var value = utils.getInputTextValue(element, event);
                if(value === undefined) {
                    return undefined;
                }
                try {
                    new RegExp(value);
                }
                catch(e) {
                    inputError(element, event);
                    return undefined;
                }
                return value;
            };

            // Return input value and check that it's a valid JavaScript object
            utils.getInputJsValue = function(element, event) {
                element = jqElt(element);
                var value = utils.getInputTextValue(element, event);
                if(value === undefined) {
                    return undefined;
                }
                try {
                    /*jshint evil:true */
                    eval("var test=" + value);
                    /*jshint evil:false */
                }
                catch(e) {
                    inputError(element, event);
                    return undefined;
                }
                return value;
            };

            // Return input value and check that it's a valid JSON
            utils.getInputJSONValue = function(element, event) {
                element = jqElt(element);
                var value = utils.getInputTextValue(element, event);
                if(value === undefined) {
                    return undefined;
                }
                try {
                    JSON.parse(value);
                }
                catch(e) {
                    inputError(element, event);
                    return undefined;
                }
                return value;
            };

            // Return checkbox boolean value
            utils.getInputChecked = function(element) {
                element = jqElt(element);
                return element.prop("checked");
            };

            // Set checkbox state
            utils.setInputChecked = function(element, checked) {
                element = jqElt(element);
                element.prop("checked", checked).change();
            };

            // Get radio button value
            utils.getInputRadio = function(name) {
                return $("input:radio[name=" + name + "]:checked").prop("value");
            };

            // Set radio button value
            utils.setInputRadio = function(name, value) {
                $("input:radio[name=" + name + "][value=" + value + "]").prop("checked", true).change();
            };

            // Reset input control in all modals
            utils.resetModalInputs = function() {
                $(".modal input[type=text]:not([disabled]), .modal input[type=password], .modal textarea").val("");
                $(".modal input[type=checkbox]").prop("checked", false).change();
            };

            // Basic trim function
            utils.trim = function(str) {
                return $.trim(str);
            };

            // Slug function
            var nonWordChars = XRegExp('[^\\p{L}\\p{N}-]', 'g');
            utils.slugify = function(text) {
                return text.toLowerCase().replace(/\s/g, '-') // Replace spaces with -
                    .replace(nonWordChars, '') // Remove all non-word chars
                    .replace(/\-\-+/g, '-') // Replace multiple - with single -
                    .replace(/^-+/, '') // Trim - from start of text
                    .replace(/-+$/, ''); // Trim - from end of text
            };

            // Check an URL
            utils.checkUrl = function(url, addSlash) {
                if(!url) {
                    return url;
                }
                if(url.indexOf("http") !== 0) {
                    url = "http://" + url;
                }
                if(addSlash && url.indexOf("/", url.length - 1) === -1) {
                    url += "/";
                }
                return url;
            };

            // Create the modal element and add to the body
            utils.addModal = function(id, content) {
                var modal = crel('div', {
                    class: 'modal ' + id
                });
                modal.innerHTML = content;
                document.body.appendChild(modal);
            };

            // Create a backdrop and add to the body
            utils.createBackdrop = function(parent) {
                var result = crel('div', {
                    'class': 'modal-backdrop fade'
                });
                parent = parent || document.body;
                parent.appendChild(result);
                result.offsetWidth; // force reflow
                result.className = result.className + ' in';
                result.removeBackdrop = function() {
                    result.className = 'modal-backdrop fade';
                    setTimeout(function() {
                        result.parentNode.removeChild(result);
                    }, 150);
                };
                return result;
            };

            var openedTooltip;
            utils.createTooltip = function(selector, content) {
                _.each(document.querySelectorAll(selector), function(tooltipElt) {
                    var $tooltipElt = $(tooltipElt);
                    $tooltipElt.tooltip({
                        html: true,
                        container: $tooltipElt.parents('.modal-content'),
                        placement: 'right',
                        trigger: 'manual',
                        title: content
                    }).click(function() {
                        var elt = this;
                        if(openedTooltip && openedTooltip[0] === elt) {
                            return;
                        }
                        utils.defer(function() {
                            openedTooltip = $(elt).tooltip('show');
                        });
                    });
                });
            };

            // Create an centered popup window
            utils.popupWindow = function(url, title, width, height) {
                var left = (screen.width / 2) - (width / 2);
                var top = (screen.height / 2) - (height / 2);
                return window.open(url, title, [
                        'toolbar=no, ',
                        'location=no, ',
                        'directories=no, ',
                        'status=no, ',
                        'menubar=no, ',
                        'scrollbars=no, ',
                        'resizable=no, ',
                        'copyhistory=no, ',
                        'width=' + width + ', ',
                        'height=' + height + ', ',
                        'top=' + top + ', ',
                        'left=' + left
                        ].join(""));
            };

            var $windowElt = $(window);
            utils.iframe = function(url, width, height) {
                var $backdropElt = $(utils.createBackdrop());
                var result = crel('iframe', {
                    src: url,
                    frameborder: 0,
                    class: 'modal-content modal-iframe'
                });
                document.body.appendChild(result);
                function placeIframe() {
                    var actualWidth = window.innerWidth - 20;
                    actualWidth > width && (actualWidth = width);
                    var actualHeight = window.innerHeight - 50;
                    actualHeight > height && (actualHeight = height);
                    result.setAttribute('width', actualWidth);
                    result.setAttribute('height', actualHeight);
                }
                placeIframe();
                $windowElt.on('resize.iframe', placeIframe);
                function removeIframe() {
                    $backdropElt.off('click.backdrop');
                    $backdropElt[0].removeBackdrop();
                    $windowElt.off('resize.iframe');
                    result.parentNode.removeChild(result);
                }
                result.removeIframe = removeIframe;
                $backdropElt.on('click.backdrop', removeIframe);
                return result;
            };

            // Shows a dialog to force the user to click a button before opening oauth popup
            var redirectCallbackConfirm;
            var redirectCallbackCancel;
            utils.redirectConfirm = function(message, callbackConfirm, callbackCancel) {
                redirectCallbackConfirm = callbackConfirm;
                redirectCallbackCancel = callbackCancel;
                $('.modal-redirect-confirm .redirect-msg').html(message);
                $('.modal-redirect-confirm').modal("show");
            };

            utils.init = function() {
                $('.action-redirect-confirm').click(function() {
                    redirectCallbackCancel = undefined;
                    redirectCallbackConfirm();
                });
                $('.modal-redirect-confirm').on('hidden.bs.modal', function() {
                    _.defer(function() {
                        redirectCallbackCancel && redirectCallbackCancel();
                    });
                });
                $(document).on('click', function() {
                    // Close opened tooltip if any
                    openedTooltip && openedTooltip.tooltip('hide');
                    openedTooltip = undefined;
                });
                $(document).on('click', '.tooltip', function(evt) {
                    // Avoid tooltip to close when clicking inside
                    evt.stopPropagation();
                });
            };

            var entityMap = {
                "&": "&amp;",
                "<": "&lt;",
                //">": "&gt;",
                '"': '&quot;',
                "'": '&#39;',
                "/": '&#x2F;',
                "\u00a0": ' '
            };

            // Escape HTML entities
            utils.escape = function(str) {
                return String(str).replace(/[&<"'\/\u00a0]/g, function(s) {
                        return entityMap[s];
                        });
            };

            // Export data on disk
            utils.saveAs = function(content, filename) {
                if(saveAs !== undefined && !/constructor/i.test(window.HTMLElement) /* safari does not support saveAs */) {
                    if(_.isString(content)) {
                        content = new Blob([
                                content
                                ], {
                                    type: "text/plain;charset=utf-8"
                                });
                    }
                    saveAs(content, filename);
                }
                else {
                    if(_.isString(content)) {
                        var uriContent = "data:application/octet-stream;base64," + utils.encodeBase64(content);
                        window.open(uriContent, 'file');
                    }
                    else {
                        var reader = new FileReader();
                        reader.onload = function(event) {
                            utils.redirectConfirm('You are opening a PDF document.', function() {
                                var uriContent = 'data:application/pdf;' + event.target.result.substring(event.target.result.indexOf('base64'));
                                window.open(uriContent, 'file');
                            });
                        };
                        reader.readAsDataURL(content); // Convert the blob to base64
                    }
                }
            };

            // Time shared by others modules
            utils.updateCurrentTime = function() {
                utils.currentTime = Date.now();
            };
            utils.updateCurrentTime();

            // Serialize sync/publish attributes and store it in the storage
            utils.storeAttributes = function(attributes) {
                var storeIndex = attributes.syncIndex || attributes.publishIndex;
                // Don't store sync/publish index
                var storedAttributes = _.omit(attributes, "syncIndex", "publishIndex", "provider");
                // Store providerId instead of provider
                storedAttributes.provider = attributes.provider.providerId;
                storage[storeIndex] = JSON.stringify(storedAttributes);
            };

            // Retrieve/parse an index array from storage
            utils.retrieveIndexArray = function(storeIndex) {
                try {
                    return _.compact(storage[storeIndex].split(";"));
                }
                catch(e) {
                    storage[storeIndex] = ";";
                    return [];
                }
            };

            // Append an index to an array in storage
            utils.appendIndexToArray = function(storeIndex, index) {
                storage[storeIndex] += index + ";";
            };

            // Remove an index from an array in storage
            utils.removeIndexFromArray = function(storeIndex, index) {
                storage[storeIndex] = storage[storeIndex].replace(";" + index + ";", ";");
            };

            // Retrieve/parse an object from storage. Returns undefined if error.
            utils.retrieveIgnoreError = function(storeIndex) {
                try {
                    return JSON.parse(storage[storeIndex]);
                }
                catch(e) {
                    return undefined;
                }
            };

            var eventList = [];
            utils.logValue = function(value) {
                eventList.unshift(value);
                if(eventList.length > 5) {
                    eventList.pop();
                }
            };
            utils.logStackTrace = function() {
                eventList.unshift(printStackTrace());
                if(eventList.length > 5) {
                    eventList.pop();
                }
            };
            utils.formatEventList = function() {
                var result = [];
                _.each(eventList, function(event) {
                    result.push("\n");
                    if(_.isString(event)) {
                        result.push(event);
                    }
                    else if(_.isArray(event)) {
                        result.push(event[5] || "");
                        result.push(event[6] || "");
                    }
                });
                return result.join("");
            };

            // Base64 conversion
            utils.encodeBase64 = function(str) {
                if(str.length === 0) {
                    return "";
                }

                // UTF-8 to byte array
                var bytes = [], offset = 0, length, char;

                str = encodeURI(str);
                length = str.length;

                while(offset < length) {
                    char = str[offset];
                    offset += 1;

                    if('%' !== char) {
                        bytes.push(char.charCodeAt(0));
                    }
                    else {
                        char = str[offset] + str[offset + 1];
                        bytes.push(parseInt(char, 16));
                        offset += 2;
                    }
                }

                // byte array to base64
                var padchar = '=';
                var alpha = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';

                var i, b10;
                var x = [];

                var imax = bytes.length - bytes.length % 3;

                for(i = 0; i < imax; i += 3) {
                    b10 = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2];
                    x.push(alpha.charAt(b10 >> 18));
                    x.push(alpha.charAt((b10 >> 12) & 0x3F));
                    x.push(alpha.charAt((b10 >> 6) & 0x3f));
                    x.push(alpha.charAt(b10 & 0x3f));
                }
                switch(bytes.length - imax) {
                    case 1:
                        b10 = bytes[i] << 16;
                        x.push(alpha.charAt(b10 >> 18) + alpha.charAt((b10 >> 12) & 0x3F) + padchar + padchar);
                        break;
                    case 2:
                        b10 = (bytes[i] << 16) | (bytes[i + 1] << 8);
                        x.push(alpha.charAt(b10 >> 18) + alpha.charAt((b10 >> 12) & 0x3F) + alpha.charAt((b10 >> 6) & 0x3f) + padchar);
                        break;
                }
                return x.join('');
            };

            // CRC32 algorithm
            var mHash = [
                0,
                1996959894,
                3993919788,
                2567524794,
                124634137,
                1886057615,
                3915621685,
                2657392035,
                249268274,
                2044508324,
                3772115230,
                2547177864,
                162941995,
                2125561021,
                3887607047,
                2428444049,
                498536548,
                1789927666,
                4089016648,
                2227061214,
                450548861,
                1843258603,
                4107580753,
                2211677639,
                325883990,
                1684777152,
                4251122042,
                2321926636,
                335633487,
                1661365465,
                4195302755,
                2366115317,
                997073096,
                1281953886,
                3579855332,
                2724688242,
                1006888145,
                1258607687,
                3524101629,
                2768942443,
                901097722,
                1119000684,
                3686517206,
                2898065728,
                853044451,
                1172266101,
                3705015759,
                2882616665,
                651767980,
                1373503546,
                3369554304,
                3218104598,
                565507253,
                1454621731,
                3485111705,
                3099436303,
                671266974,
                1594198024,
                3322730930,
                2970347812,
                795835527,
                1483230225,
                3244367275,
                3060149565,
                1994146192,
                31158534,
                2563907772,
                4023717930,
                1907459465,
                112637215,
                2680153253,
                3904427059,
                2013776290,
                251722036,
                2517215374,
                3775830040,
                2137656763,
                141376813,
                2439277719,
                3865271297,
                1802195444,
                476864866,
                2238001368,
                4066508878,
                1812370925,
                453092731,
                2181625025,
                4111451223,
                1706088902,
                314042704,
                2344532202,
                4240017532,
                1658658271,
                366619977,
                2362670323,
                4224994405,
                1303535960,
                984961486,
                2747007092,
                3569037538,
                1256170817,
                1037604311,
                2765210733,
                3554079995,
                1131014506,
                879679996,
                2909243462,
                3663771856,
                1141124467,
                855842277,
                2852801631,
                3708648649,
                1342533948,
                654459306,
                3188396048,
                3373015174,
                1466479909,
                544179635,
                3110523913,
                3462522015,
                1591671054,
                702138776,
                2966460450,
                3352799412,
                1504918807,
                783551873,
                3082640443,
                3233442989,
                3988292384,
                2596254646,
                62317068,
                1957810842,
                3939845945,
                2647816111,
                81470997,
                1943803523,
                3814918930,
                2489596804,
                225274430,
                2053790376,
                3826175755,
                2466906013,
                167816743,
                2097651377,
                4027552580,
                2265490386,
                503444072,
                1762050814,
                4150417245,
                2154129355,
                426522225,
                1852507879,
                4275313526,
                2312317920,
                282753626,
                1742555852,
                4189708143,
                2394877945,
                397917763,
                1622183637,
                3604390888,
                2714866558,
                953729732,
                1340076626,
                3518719985,
                2797360999,
                1068828381,
                1219638859,
                3624741850,
                2936675148,
                906185462,
                1090812512,
                3747672003,
                2825379669,
                829329135,
                1181335161,
                3412177804,
                3160834842,
                628085408,
                1382605366,
                3423369109,
                3138078467,
                570562233,
                1426400815,
                3317316542,
                2998733608,
                733239954,
                1555261956,
                3268935591,
                3050360625,
                752459403,
                1541320221,
                2607071920,
                3965973030,
                1969922972,
                40735498,
                2617837225,
                3943577151,
                1913087877,
                83908371,
                2512341634,
                3803740692,
                2075208622,
                213261112,
                2463272603,
                3855990285,
                2094854071,
                198958881,
                2262029012,
                4057260610,
                1759359992,
                534414190,
                2176718541,
                4139329115,
                1873836001,
                414664567,
                2282248934,
                4279200368,
                1711684554,
                285281116,
                2405801727,
                4167216745,
                1634467795,
                376229701,
                2685067896,
                3608007406,
                1308918612,
                956543938,
                2808555105,
                3495958263,
                1231636301,
                1047427035,
                2932959818,
                3654703836,
                1088359270,
                936918000,
                2847714899,
                3736837829,
                1202900863,
                817233897,
                3183342108,
                3401237130,
                1404277552,
                615818150,
                3134207493,
                3453421203,
                1423857449,
                601450431,
                3009837614,
                3294710456,
                1567103746,
                711928724,
                3020668471,
                3272380065,
                1510334235,
                755167117
                    ];
            utils.crc32 = function(str) {
                var n = 0, crc = -1;
                str.split('').forEach(function(char) {
                    n = (crc ^ char.charCodeAt(0)) & 0xFF;
                    crc = (crc >>> 8) ^ mHash[n];
                });
                crc = crc ^ (-1);
                if(crc < 0) {
                    crc = 0xFFFFFFFF + crc + 1;
                }
                return crc.toString(16);
            };

            window.perfTest = function(cb) {
                var startTime = Date.now();
                for(var i = 0; i < 10000; i++) {
                    cb();
                }
                console.log('Run 10,000 times in ' + (Date.now() - startTime) + 'ms');
            };

            return utils;
        });
